/*
 * Copyright 2016-2019 yoara
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package yhao.infra.service.datasource.stat;

import ch.qos.logback.classic.Level;
import com.alibaba.druid.stat.DruidStatManagerFacade;
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import yhao.infra.common.YhaoInfraCommonProperties;
import yhao.infra.service.YhaoInfraServiceProperties;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * druid统计实现组件
 *
 * @author yoara
 */
@Component
public class DruidStatSqlLogCollector implements InitializingBean {
    @Autowired
    private Environment env;

    private Logger logger;
    private DruidStatManagerFacade statManagerFacade;
    private ScheduledExecutorService executor;
    private String appName;


    @Override
    public void afterPropertiesSet() throws Exception {
        appName = env.getProperty(YhaoInfraCommonProperties.ENV_APP_NAME,"undefined");
        String loggerPath = env.getProperty(YhaoInfraServiceProperties.DRUID_STAT_SQL_LOG_PATH,"/log/");
        statManagerFacade = DruidStatManagerFacade.getInstance();
        SQL_LOG_BASE = (isSystemWindows() ? "c:" : "")
                + loggerPath + "druid_sql_stat_logs/"
                + appName + env.getProperty("server.port","");
        logger = builderSqlStatLogger(DruidStatSqlLogCollector.class);
        executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleAtFixedRate(() -> getSqlStatLog(), getNextHourTimespan(), 60 * 60, TimeUnit.SECONDS);
    }

    public void getSqlStatLog() {
        try {
            Calendar cal = Calendar.getInstance();
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
            List<SqlStatInfo> data = getSqlStatDataList();
            Map<String, Object> logMap = new HashMap<>();
            logMap.put(APP_NAME, appName);
            cal.add(Calendar.HOUR, -1);
            logMap.put(STAT_DATE, format.format(cal.getTime()));
            logMap.put(STAT_HOUR, cal.get(Calendar.HOUR_OF_DAY));
            logMap.put("data", data);
            String logText = JSON.toJSONString(logMap);
            logger.info(logText);
        } catch (Throwable e) {
            logger.error(e.getMessage(), e);
        }
    }

    public List<SqlStatInfo> getSqlStatDataList() {
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        List<SqlStatInfo> result = new ArrayList<>();
        List<Map<String, Object>> array = getJdbcSqlStatDataList();
        for (Map<String, Object> map : array) {
            SqlStatInfo data = new SqlStatInfo();
            result.add(data);
            data.setConcurrentMax((Long) map.get("ConcurrentMax"));
            data.setEffectedRowCount((Long) map.get("EffectedRowCount"));
            data.setEffectedRowCountMax((Long) map.get("EffectedRowCountMax"));
            data.setErrorCount((Long) map.get("ErrorCount"));
            data.setExecuteCount((Long) map.get("ExecuteCount"));
            data.setFetchRowCount((Long) map.get("FetchRowCount"));
            data.setFetchRowCountMax((Long) map.get("FetchRowCountMax"));
            long[] hs = (long[]) map.get("Histogram");
            if (hs.length == 8) {
                data.setHistogram0(hs[0]);
                data.setHistogram1(hs[1]);
                data.setHistogram2(hs[2]);
                data.setHistogram3(hs[3]);
                data.setHistogram4(hs[4]);
                data.setHistogram5(hs[5]);
                data.setHistogram6(hs[6]);
                data.setHistogram7(hs[7]);
            }

            //data.setLastError( (Throwable)map.get("LastError"));
            data.setLastSlowParameters(map.get("LastSlowParameters"));
            data.setMaxTimespan((Long) map.get("MaxTimespan"));
            Date maxTimespanOccurTime = (Date) map.get("MaxTimespanOccurTime");
            if (maxTimespanOccurTime != null) {
                data.setMaxTimespanOccurTime(format.format(maxTimespanOccurTime));
            }
            String sql = map.get("SQL").toString().trim();
            data.setTableName(getTableName(sql));
            data.setSql(sql);
            data.setTotalTime((Long) map.get("TotalTime"));
        }
        return result;
    }

    private String getTableName(String sql) {
        if (sql == null) {
            return "";
        }
        Matcher matcher = null;
        String sql0 = sql.toLowerCase().trim();
        if (sql0.startsWith("insert")) {
            matcher = insertSqlPattern.matcher(sql0);
        } else if (sql0.startsWith("update")) {
            matcher = updateSqlPattern.matcher(sql0);
        } else if (sql0.startsWith("delete")) {
            matcher = deleteSqlPattern.matcher(sql0);
        } else if (sql0.startsWith("select")) {
            matcher = selectSqlPattern.matcher(sql0);
        }
        if (matcher != null && matcher.find()) {
            return (matcher.group(1));
        }
        return "";
    }

    public static long getNextHourTimespan() {
        Calendar cal = Calendar.getInstance();
        long now = cal.getTime().getTime();
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.add(Calendar.HOUR, 1);
        long timespan = (cal.getTime().getTime() - now) / 1000 + 1;
        return timespan;
    }

    private List<Map<String, Object>> getJdbcSqlStatDataList() {
        Integer dataSourceId = null;
        List<Map<String, Object>> array = statManagerFacade.getSqlStatDataList(dataSourceId);
        statManagerFacade.resetSqlStat();
        return array;
    }

    /**
     * 构造sql stat的自定义Logger
     * @param clazz
     * @return
     */
    public Logger builderSqlStatLogger(Class clazz){
        return LoggerUtil.getLogger(DATETIME_MSG_FORMAT,Level.INFO.toString(),"500MB", SQL_LOG_BASE, "sqlstat", clazz);
    }

    /**
     * 当前应用是否部署在windows下
     */
    private static Boolean isSystemWindows() {
        if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    private String SQL_LOG_BASE;
    private static final String DATETIME_MSG_FORMAT = "%d{yyyy-MM-dd HH:mm:ss}  %m%n";

    private final static String insertSqlRegex = "insert[\\S\\s]*into\\s*\\(*([^\\(^\\)^\\s^\'^\"^\\,]*)[\\S\\s]*";
    private final static Pattern insertSqlPattern = Pattern.compile(insertSqlRegex, Pattern.CASE_INSENSITIVE);

    private final static String updateSqlRegex = "update\\s*\\(*([^\\(^\\)^\\s^\'^\"^\\,]*)[\\S\\s]*";
    private final static Pattern updateSqlPattern = Pattern.compile(updateSqlRegex, Pattern.CASE_INSENSITIVE);

    private final static String selectSqlRegex = "select[\\S\\s]*from\\s*\\(*([^\\(^\\)^\\s^\'^\"^\\,]*)[\\S\\s]*";
    private final static Pattern selectSqlPattern = Pattern.compile(selectSqlRegex, Pattern.CASE_INSENSITIVE);

    private final static String deleteSqlRegex = "delete[\\S\\s]*from\\s*\\(*([^\\(^\\)^\\s^\'^\"^\\,]*)[\\S\\s]*";
    private final static Pattern deleteSqlPattern = Pattern.compile(deleteSqlRegex, Pattern.CASE_INSENSITIVE);

    public final static String APP_NAME = "appName";
    public final static String STAT_DATE = "statDate";
    public final static String STAT_HOUR = "statHour";
}
